#!/bin/sh -ef
#
# Copyright (C) 2003-2019  Dmitry V. Levin <ldv@altlinux.org>
# Copyright (C) 2006  Alexey Gladkov <legion@altlinux.org>
# Copyright (C) 2007  Kirill A. Shutemov <kas@altlinux.org>
# 
# This file defines functions used by hasher scripts.
#
# SPDX-License-Identifier: GPL-2.0-or-later
#

unset CDPATH ||:
PROG="${0##*/}"

. shell-error
. shell-quote
. shell-args

quiet=
verbose=

# Create Var_of_$1 variable and set it to $2 if exist, otherwise to $1
Helpify() # var_name=var_value [cmdline_key_name]
{
	eval "Var_of_${1%%=*}='
                                    (\$${2:-${1%%=*}})'"
	eval "hsh_cfg_var_${2:-${1%%=*}}="
}

print_version()
{
	local prog
	prog="$1" && shift
	cat <<EOF
$prog version @VERSION@
Written by Dmitry V. Levin <ldv@altlinux.org>

Copyright (C) 2003-2017  Dmitry V. Levin <ldv@altlinux.org>
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
	exit
}

# safe umask.
umask 022

# save current work directory.
saved_cwd="$(/bin/pwd)"

### Begin configuration variables

# path to custom apt.conf file
apt_config=
Helpify apt_config

# path to apt directory prefix (e.g. /usr)
apt_prefix=
Helpify apt_prefix

# Whether build environment should be deleted after
# each successful build or before each new build
lazy_cleanup=
Helpify lazy_cleanup
Helpify eager_cleanup lazy_cleanup

# RPM --excludedocs
exclude_docs=
Helpify excludedocs exclude_docs

# hasher-priv directory.
def_hasher_priv_dir=@libexecdir@/hasher-priv
hasher_priv_dir=
Helpify hasher_priv_dir def_hasher_priv_dir

# colon-separated list of languages to install
def_install_langs=all
install_langs=
Helpify install_langs def_install_langs

# comma-separated list of known mount points
known_mountpoints=
Helpify mountpoints known_mountpoints

# whether to wait for lock
lock_nowait=
Helpify wait_lock lock_nowait
Helpify no_wait_lock lock_nowait

# override default cache directory $workdir/cache
cache_dir=
Helpify cache_dir

# override default cache compressor
cache_compress=
Helpify cache_compress

# whether to use initroot cache
no_cache=
Helpify no_cache

# Whether the initroot cache is unchecked.
# Unchecked initroot cache allows
# more efficient initroot caching, but
# its validity is responsibility of the user.
unchecked_initroot_cache=
Helpify unchecked_initroot_cache

# whether to use content indices
no_contents_indices=
Helpify no_contents_indices

# default sisyphus_check config
no_sisyphus_check=
Helpify no_sisyphus_check
no_sisyphus_check_in=
Helpify no_sisyphus_check_in
no_sisyphus_check_out=
Helpify no_sisyphus_check_out

# number of CPUs to use
nprocs=
Helpify nprocs

# subconfig identifier
number=
Helpify number

# override default packager <hasher@localhost>
packager=
Helpify packager

# whether repackage source along with binaries
repackage_source='--repackage-source'
Helpify repackage_source
Helpify no_repackage_source repackage_source

# build stage package file list
pkg_build_list='basesystem rpm-build>=0:4.0.4-alt21 kernel-headers-common>=0:1.1.4-alt1 sisyphus_check>=0:0.7.3 time'
build_list=
Helpify pkg_build_list

# initial stage package file list
pkg_init_list='setup filesystem rpm fakeroot>=0:0.7.3'
init_list=
Helpify pkg_init_list

# program to run to query for requirements instead of autogenerted script
prog_query_req=
Helpify query_req_prog prog_query_req

# program to run for rebuild instead of autogenerated script
prog_rebuild=
Helpify rebuild_prog prog_rebuild

# whether to repackage the source before query for requirements
query_repackage=1
Helpify query_repackage

# noinstall package pattern list
noinstall_pattern_list='dev[-_][0-9]* dev[-_]minimal[-_][0-9]*'
Helpify pkg_noinstall_pattern_list noinstall_pattern_list

# repo directory.
def_repo=repo
repo=
Helpify repo def_repo

# repo-bin directory.
repo_bin=
Helpify repo_bin

# repo-src directory.
repo_src=
Helpify repo_src

# extra arguments for rpmbuild
rpmargs=
Helpify rpmbuild_args rpmargs

# path to rpmi
def_rpmi=rpmi
rpmi=

# whether to save fakeroot state
save_fakeroot=
Helpify save_fakeroot

# whether to copy /etc/hosts, /etc/host.conf and /etc/resolv.conf from host system
install_resolver_configuration_files=
Helpify install_resolver_configuration_files

# target architecture.
def_target="$(rpm --showrc |sed -ne 's/^install arch[[:space:]]*:[[:space:]]*\([^[:space:]]\+\).*/\1/p')"
[ -n "$def_target" ] || def_target="$(uname -m)"
target=
Helpify target def_target

# Whether to use results of previous builds stored in
# $workdir/$repo during setup of new build environment
no_stuff=
Helpify with_stuff no_stuff
Helpify without_stuff no_stuff

# qemu architecture
qemu_arch=
Helpify qemu_arch

# various reasonable work limits.
wlimit_time_short=60
Helpify wlimit_time_short
wlimit_time_long=600
Helpify wlimit_time_long
wlimit_bytes_out=65536
Helpify wlimit_bytes_out

# working directory.
workdir="$HOME/hasher"
Helpify workdir

### End configuration variables

# source user config if any
hasher_config="$HOME/.hasher/config"
if [ -s "$hasher_config" ]; then
	. "$hasher_config"
fi

# APT workdir, $workdir/aptbox
aptbox=

# chroot workdir, $workdir/chroot
chroot=

# program to execute while entering chroot, $chroot/.host/entry
entry=

# variables used by hasher-priv
requested_mountpoints=
wlimit_time_elapsed=
wlimit_time_idle=
wlimit_bytes_written=
use_pty=

# Checks that given option value is a non-negative decimal number.
# Arguments: $1 is option name, $2 is option value.
# If $2 is a non-negative decimal number, outputs it, otherwise fails.
opt_check_number_ge_0()
{
	if [ "${2-}" = 0 ]; then
		printf 0
	else
		opt_check_number "$@"
	fi
}

set_workdir()
{
	[ -n "$workdir" -a -z "${1:-}" ] ||
		workdir="${1:-}"
	cd "$workdir" || return 1
	workdir="$(/bin/pwd)" || return 1

	[ -n "$(printf %s "$workdir" |tr -d /.)" ] ||
		fatal "$workdir: illegal working directory."
	if printf %s "$workdir" |LC_ALL=C grep -qs '[`"$\]'; then
		fatal "$workdir: illegal symbols in pathname."
	fi
	[ -w . ] || fatal "$workdir: unwritable working directory."

	verbose "changed working directory to \`$workdir'"
	aptbox="$workdir/aptbox"
	chroot="$workdir/chroot"
	cache_dir="${cache_dir:-$workdir/cache}"
	entry="$chroot/.host/entry"
}

# assumes: initialized $chroot
install_static_helper()
{
	local name="$1"
	local src="$name.static"
	local dst="${2:-$name}"
	local path="$(type -p "$src")"
	[ -n "$path" -a -z "${path##/*}" ] ||
		fatal "Static helper $src not found."
	install -p -m755 $verbose "$path" "$chroot/.host/$dst" >&2
}

# assumes: defined aptbox
get_apt_config()
{
	local get_eval get_name get_value
	get_name="$1"
	shift || return 1
	get_value="$1"
	shift || return 1

	get_eval="$("$aptbox/apt-config" shell "$get_value" "$get_name" </dev/null)" || return 1
	eval "$get_eval"
}

hash=

# set hash variable.
check_tty()
{
	[ -n "$verbose" ] && tty -s <&1 &&
		hash=-h ||
		hash=
}

check_helpers_done=
check_helpers()
{
	[ -z "$check_helpers_done" ] || return 0

	[ -d "${hasher_priv_dir:-$def_hasher_priv_dir}" ] ||
		fatal "${hasher_priv_dir:-$def_hasher_priv_dir}: cannot access hasher-priv helper directory."

	getconf="${hasher_priv_dir:-$def_hasher_priv_dir}/getconf.sh"
	[ -x "$getconf" ] ||
		fatal "$getconf: cannot access getconf helper."

	getugid1="${hasher_priv_dir:-$def_hasher_priv_dir}/getugid1.sh"
	[ -x "$getugid1" ] ||
		fatal "$getugid1: cannot access getugid1 helper."

	getugid2="${hasher_priv_dir:-$def_hasher_priv_dir}/getugid2.sh"
	[ -x "$getugid2" ] ||
		fatal "$getugid2: cannot access getugid2 helper."

	chrootuid1="${hasher_priv_dir:-$def_hasher_priv_dir}/chrootuid1.sh"
	[ -x "$chrootuid1" ] ||
		fatal "$chrootuid1: cannot access chrootuid1 helper."

	chrootuid2="${hasher_priv_dir:-$def_hasher_priv_dir}/chrootuid2.sh"
	[ -x "$chrootuid2" ] ||
		fatal "$chrootuid2: cannot access chrootuid2 helper."

	check_helpers_done=1
}

# assumed: cwd == workdir
purge_chroot_in()
{
	find chroot/.in/ -mindepth 1 -depth -delete
}

# assumed: cwd == workdir
purge_chroot_out()
{
	find chroot/.out/ -mindepth 1 -depth -delete
}

# assumed: cwd == workdir
copy_chroot_incoming()
{
	purge_chroot_in
	local f err=
	for f; do
		install -p -m644 $verbose -- "$f" chroot/.in/ >&2 || err=1
	done
	[ -z "$err" ] || fatal 'failed to copy files.'
}

# assumed: cwd == workdir
make_repo()
{
	if [ -n "$repo_src" -o -n "$repo_bin" ]; then
		mkdir -p $verbose -- ${repo_src:+"$repo_src"} ${repo_bin:+"$repo_bin"} >&2
	fi
	mkdir -p $verbose -- ${repo:-$def_repo}/{SRPMS,${target:-$def_target}/RPMS}.hasher >&2
}

# assumed: defined variables: hash
update_RPM_database()
{
	local extra_args=
	while [ -n "${1-}" -a -z "${1##-*}" ]; do
		extra_args="$extra_args $1"
		shift
	done
	for f; do
		printf '%s\n' "$f"
	done |sed 's/[][?*\]/\\&/g' >"$aptbox/rpmi.list"
	"$aptbox/setarch" rpmi -i $verbose $hash ${exclude_docs:+--excludedocs} $extra_args --dbpath "$aptbox/var/lib/rpm" \
	     --ignorearch --ignoresize --noorder --noscripts --notriggers --justdb \
	     "$aptbox/rpmi.list" &&
		verbose 'RPM database updated.' ||
		fatal 'RPM database update failed.'
	rm "$aptbox/rpmi.list"
}

# execute as pseudoroot.
chrootuid1()
{
	[ $# -gt 0 ] || set -- /.host/entry
	local rc=0
	if [ -x "$aptbox/setarch" ]; then
		"$aptbox/setarch" "$chrootuid1" ${number:+-$number} "$chroot" "$@"
	else
		"$chrootuid1" ${number:+-$number} "$chroot" "$@"
	fi || rc=$?
	wlimit_time_elapsed= wlimit_time_idle= wlimit_bytes_written=
	return $rc
}

# execute as builder.
chrootuid2()
{
	[ $# -gt 0 ] || set -- /.host/entry
	local rc=0
	if [ -x "$aptbox/setarch" ]; then
		"$aptbox/setarch" "$chrootuid2" ${number:+-$number} "$chroot" "$@"
	else
		"$chrootuid2" ${number:+-$number} "$chroot" "$@"
	fi || rc=$?
	wlimit_time_elapsed= wlimit_time_idle= wlimit_bytes_written=
	return $rc
}

create_entry_header()
{
	cat >"$entry" <<__EOF__
#!/bin/sh -e
TMPDIR="\$HOME/tmp"
export TMPDIR
cd /.in
__EOF__
	chmod 755 "$entry"
}

create_entry_fakeroot_header()
{
	cat >"$entry" <<__EOF__
#!/bin/sh -e
if [ -z "\$FAKEROOTKEY" -a "\$USER" = root -a -x /usr/bin/fakeroot ]; then
	if [ -f '/.fakedata' ]; then
		exec /usr/bin/fakeroot -i /.fakedata -s /.fakedata "\$0" "\$@"
	else
		exec /usr/bin/fakeroot "\$0" "\$@"
	fi
fi
cd /.in
__EOF__
	chmod 755 "$entry"
}

no_lock=
workdir_is_locked=
lock_workdir()
{
	[ -z "$workdir_is_locked" ] || return 0
	workdir_is_locked=1
	[ -z "$no_lock" ] || return 0

	enable -f /usr/lib/bash/lockf lockf
	if builtin lockf -n "$workdir"; then
		echo $$ >"$workdir/pid"
		verbose "Locked working directory \`$workdir'"
		return 0
	fi

	local pid
	if [ -n "$verbose" -o -n "$lock_nowait" ] &&
	   pid="$(head -c32 -- "$workdir/pid")" &&
	   [ "$pid" -gt 0 ] 2>/dev/null; then
		if kill -0 -- "$pid" 2>/dev/null; then
			message "working directory \`$workdir' is already locked by pid=$pid."
			a= ps hp "$pid" >&2 ||:
		else
			message "working directory \`$workdir' is already locked by stale pid=$pid."
		fi
	fi

	if [ -z "$lock_nowait" ]; then
		verbose "Waiting for working directory lock..."
		if builtin lockf "$workdir"; then
			echo $$ >"$workdir/pid"
			verbose "Locked working directory \`$workdir'"
			return 0
		fi
	fi

	fatal "Unable to lock working directory \`$workdir'"
}

# assumed: variables initialized by check_helpers()
config_is_locked=
lock_hasher_config()
{
	[ -z "$no_lock" -a -z "$config_is_locked" ] || return 0

	local file
	file="$("$getconf" ${number:+-$number})" ||
		fatal 'hasher-priv getconf failed.'

	enable -f /usr/lib/bash/lockf lockf
	local lock_param=
	[ -z "$lock_nowait" ] || lock_param=-n
	verbose "Acquiring lock for config file \`$file'..."
	if builtin lockf $lock_param "$file"; then
		config_is_locked=1
		verbose "Locked config file \`$file'"
		return 0
	else
		verbose "Config file \`$file' is busy"
		return 1
	fi
}

list_user_subconfig_numbers()
{
	local n
	for n in '' $(cd /etc/hasher-priv/user.d/ && set +f && ls $(id -un):[1-9]* 2>/dev/null); do
		if [ -n "$n" ]; then
			[ -n "${n##*\*}" ] || continue
			n="${n#*:}"
			[ "$n" -gt 0 ] 2>/dev/null || continue
			echo "$n"
		else
			echo 0
		fi
	done
}

show_user_subconfig_numbers()
{
	local randomize="$1"; shift
	set -- `list_user_subconfig_numbers`
	[ $# -gt 0 ] || return 0
	[ -z "$randomize" ] || shift $((${RANDOM-0}%$#))
	echo "$@"
}

iterate_config_files_set_number()
{
	local randomize="$1"; shift
	local saved_number="$number"

	# try config files one by one
	for number in `show_user_subconfig_numbers "$randomize"`; do
		[ "$number" != 0 ] || number=
		if "$@"; then
			return 0
		fi
	done

	# if no suitable config file found, restore number clobbered by the loop
	number="$saved_number"
	return 1
}

is_config_matches_gid()
{
	local sample="$1"; shift
	local gid

	gid="$("$getugid1" ${number:+-$number} 2>/dev/null)" || return
	gid="${gid##*:}"
	[ "$sample" = "$gid" ] || return
}

# assumed: variables initialized by check_helpers()
deduce_number_from_directory()
{
	local dir="$1"; shift
	local gid

	[ -d "$dir" ] ||
		fatal "$dir: Directory not available."

	# take gid from directory ownership
	gid="$(find "$dir" -maxdepth 0 -type d -printf '%G')"
	[ "$gid" -ge 0 ] 2>/dev/null ||
		fatal "$dir: Failed to get directory ownership."

	iterate_config_files_set_number '' is_config_matches_gid "$gid" ||
		fatal 'Failed to deduce hasher config number from directory ownership.'
}

deduce_lock_hasher_priv()
{
	local dir="${1-$chroot}"
	check_helpers
	deduce_number_from_directory "$dir"
	lock_hasher_config ||
		fatal 'Failed to lock hasher-priv'
}

allocate_lock_hasher_priv()
{
	check_helpers
	[ -z "$no_lock" -a -z "$config_is_locked" ] || return 0

	# if subconfig is already defined, just use it
	if [ -n "$number" ]; then
		lock_hasher_config ||
			fatal 'Failed to lock hasher-priv'
		return 0
	fi

	# switch into non-blocking mode
	local saved_lock_nowait="$lock_nowait"
	local lock_nowait=1
	local n=0
	local randomize=1

	while :; do
		if iterate_config_files_set_number "$randomize" lock_hasher_config; then
			return 0
		fi

		# if all configs are busy and lock_nowait was set, bail out
		[ -z "$saved_lock_nowait" ] ||
			fatal 'Failed to lock hasher-priv'

		# wait for next iteration
		n="$(($n+1))"
		sleep "$n"
		randomize=
	done
}

# assumed: defined aptbox
print_uris()
{
	local out
	local options='-q -y -o RPM::PM=external -o Dir::Bin::rpm=hsh-rpmi-print-files'
	if [ -z "$verbose" ]; then
		out="$("$aptbox/apt-get" $options install -- "$@" 2>&1)" || {
			local rc=$?
			printf %s\\n "$out" >&2
			[ $rc -le 128 ] ||
				message "apt-get terminated by signal $(($rc-128))"
			fatal 'Failed to calculate package file list.'
		}
	else
		out="$(set -o pipefail; "$aptbox/apt-get" $options install -- "$@" 2>&1 |
		       tee -a /dev/stderr)" || {
			local rc=$?
			[ $rc -le 128 ] ||
				message "apt-get terminated by signal $(($rc-128))"
			fatal 'Failed to calculate package file list.'
		}
	fi

	printf %s "$out" |LC_ALL=C grep '^/.*\.rpm$' || [ $? -eq 1 ]
	verbose 'Calculated package file list.'
}

parse_xauth_entry()
{
	XAUTH_DISPLAY="$1" && shift ||:
	XAUTH_PROTO="$1" && shift ||:
	XAUTH_KEY="$1" && shift ||:

	if [ -z "$XAUTH_DISPLAY" -o -n "${XAUTH_DISPLAY##*:*}" \
	  -o -z "$XAUTH_PROTO" -o -z "$XAUTH_KEY" ]; then
		message "Invalid auth entry for DISPLAY \`$DISPLAY', disabling X11 forwarding."
		return 1
	fi

	# Workaround for broken xauth entries.
	XAUTH_DISPLAY="$DISPLAY"
}

prepare_x11_forwarding()
{
	[ -n "$x11_forwarding" ] || return 0

	if [ -z "$DISPLAY" ]; then
		message 'DISPLAY not set, disabling X11 forwarding.'
		return 1
	fi

	if [ -n "${DISPLAY##*:*}" ]; then
		message 'Invalid DISPLAY set, disabling X11 forwarding.'
		return 1
	fi

	local xauth=xauth xentry
	if [ "$x11_forwarding" = trusted ]; then
		local display
		[ -n "${DISPLAY##localhost:*}" ] &&
			display="$DISPLAY" ||
			display="unix:${DISPLAY#localhost:}"
		xentry="$(xauth list "$display")" ||
			return 1
	elif [ "$x11_forwarding" = untrusted ]; then
		local xfile
		xfile="$(mktemp -t xauth.XXXXXXXX)"
		$xauth -f "$xfile" generate "$DISPLAY" . untrusted timeout "$x11_timeout" ||
			{ rm -f "$xfile"; return 1; }
		xentry="$(xauth -f "$xfile" list "$DISPLAY")" ||
			{ rm -f "$xfile"; return 1; }
		rm -f "$xfile"
	else
		fatal 'Invalid $x11_forwarding value.'
	fi

	parse_xauth_entry $xentry || return 1

	export XAUTH_DISPLAY XAUTH_PROTO XAUTH_KEY
}

required_mountpoints=
calculate_required_mountpoints()
{
	[ -n "$known_mountpoints" ] ||
		return 0

	local deps file_deps mpoint

	deps="$(printf %s "$1" |LC_ALL=C grep ^/ |LC_ALL=C tr -d '[[:blank:]]' |LC_ALL=C sort -u)"

	for file_deps in ${deps}; do
		for mpoint in $(printf %s "$known_mountpoints" |tr -s , ' '); do
			if [ "$mpoint" = "$file_deps" ]; then
				[ -z "$required_mountpoints" ] &&
					required_mountpoints="$mpoint" ||
					required_mountpoints="$required_mountpoints,$mpoint"
				break
			fi
		done
	done

	required_mountpoints="$(printf %s "$required_mountpoints" |tr -s , '\n' |LC_ALL=C sort -u |tr -s '\n' ,)"
	required_mountpoints="${required_mountpoints%,}"
	verbose "calculated mount points: $required_mountpoints"
} # calculate_required_mountpoints

# Calculate required mountpoints from installed file dependencies.
calculate_required_mountpoints_from_installed()
{
	[ -n "$known_mountpoints" ] ||
		return 0

	create_entry_header
	cat >>"$entry" <<__EOF__
rpmquery -aR |LC_ALL=C grep ^/ |LC_ALL=C tr -d '[[:blank:]]' |sort -u
__EOF__

	local deps
	deps="$(wlimit_time_elapsed=$wlimit_time_short wlimit_time_idle=$wlimit_time_short wlimit_bytes_written=$wlimit_bytes_out \
		    chrootuid2 </dev/null)" &&
		verbose 'fetched installed file dependencies.' ||
		fatal 'failed to fetch installed file dependencies.'
	calculate_required_mountpoints "$deps"
}
